// Copyright Epic Games, Inc. All Rights Reserved.

#include "zenserver.h"

#include <zencore/compactbinarybuilder.h>
#include <zencore/compactbinaryvalidation.h>
#include <zencore/config.h>
#include <zencore/filesystem.h>
#include <zencore/fmtutils.h>
#include <zencore/logging.h>
#include <zencore/scopeguard.h>
#include <zencore/session.h>
#include <zencore/string.h>
#include <zencore/thread.h>
#include <zencore/timer.h>
#include <zencore/trace.h>
#include <zenhttp/httpserver.h>

#include "config.h"
#include "diag/logging.h"
#include "sentryintegration.h"

#if ZEN_USE_MIMALLOC
ZEN_THIRD_PARTY_INCLUDES_START
#	include <mimalloc-new-delete.h>
ZEN_THIRD_PARTY_INCLUDES_END
#endif

#if ZEN_PLATFORM_WINDOWS
#	include <zencore/windows.h>
#	include "windows/service.h"
#endif

//////////////////////////////////////////////////////////////////////////
// We don't have any doctest code in this file but this is needed to bring
// in some shared code into the executable

#if ZEN_WITH_TESTS
#	define ZEN_TEST_WITH_RUNNER 1
#	include <zencore/testing.h>
#	include <zenutil/zenutil.h>
#endif

#include <memory>

namespace zen::utils {
std::atomic_uint32_t SignalCounter[NSIG] = {0};

static void
SignalCallbackHandler(int SigNum)
{
	if (SigNum >= 0 && SigNum < NSIG)
	{
		SignalCounter[SigNum].fetch_add(1);
	}
}
}  // namespace zen::utils

namespace zen {

using namespace std::literals;

////////////////////////////////////////////////////////////////////////////////

class ZenEntryPoint
{
public:
	ZenEntryPoint(ZenServerOptions& ServerOptions);
	ZenEntryPoint(const ZenEntryPoint&) = delete;
	ZenEntryPoint& operator=(const ZenEntryPoint&) = delete;
	int			   Run();

private:
	ZenServerOptions& m_ServerOptions;
	LockFile		  m_LockFile;
};

ZenEntryPoint::ZenEntryPoint(ZenServerOptions& ServerOptions) : m_ServerOptions(ServerOptions)
{
}

int
ZenEntryPoint::Run()
{
	zen::SetCurrentThreadName("main");

#if ZEN_USE_SENTRY
	SentryIntegration Sentry;

	if (m_ServerOptions.NoSentry == false)
	{
		std::string SentryDatabasePath	 = (m_ServerOptions.DataDir / ".sentry-native").string();
		std::string SentryAttachmentPath = m_ServerOptions.AbsLogFile.string();

		Sentry.Initialize(SentryDatabasePath, SentryAttachmentPath, m_ServerOptions.SentryAllowPII);
	}
#endif

	try
	{
		// Mutual exclusion and synchronization
		ZenServerState ServerState;
		ServerState.Initialize();
		ServerState.Sweep();

		uint32_t						AttachSponsorProcessRetriesLeft = 3;
		ZenServerState::ZenServerEntry* Entry							= ServerState.Lookup(m_ServerOptions.BasePort);
		while (Entry)
		{
			if (m_ServerOptions.OwnerPid)
			{
				if (!IsProcessRunning(m_ServerOptions.OwnerPid))
				{
					ZEN_WARN("Sponsor owner pid {} is no longer running, will not add sponsor to process listening to port {} (pid: {})",
							 m_ServerOptions.OwnerPid,
							 m_ServerOptions.BasePort,
							 Entry->Pid.load());
					std::exit(1);
				}
				ZEN_INFO(
					"Looks like there is already a process listening to this port {} (pid: {}), attaching owner pid {} to running instance",
					m_ServerOptions.BasePort,
					Entry->Pid.load(),
					m_ServerOptions.OwnerPid);

				if (Entry->AddSponsorProcess(m_ServerOptions.OwnerPid, 2000))
				{
					std::exit(0);
				}
				if (AttachSponsorProcessRetriesLeft-- > 0)
				{
					Entry = ServerState.Lookup(m_ServerOptions.BasePort);
				}
				else
				{
					ZEN_WARN("Failed to add sponsor owner pid {} to process listening to port {} (pid: {})",
							 m_ServerOptions.OwnerPid,
							 m_ServerOptions.BasePort,
							 Entry->Pid.load());
					std::exit(1);
				}
			}
			else
			{
				ZEN_WARN("Exiting since there is already a process listening to port {} (pid: {})",
						 m_ServerOptions.BasePort,
						 Entry->Pid.load());
				std::exit(1);
			}
		}

		std::error_code Ec;

		std::filesystem::path LockFilePath = m_ServerOptions.DataDir / ".lock";

		auto MakeLockData = [&](bool IsReady) {
			return MakeLockFilePayload({.Pid				 = GetCurrentProcessId(),
										.SessionId			 = GetSessionId(),
										.EffectiveListenPort = gsl::narrow<uint16_t>(m_ServerOptions.BasePort),
										.Ready				 = IsReady,
										.DataDir			 = m_ServerOptions.DataDir,
										.ExecutablePath		 = GetRunningExecutablePath()});
		};

		m_LockFile.Create(LockFilePath, MakeLockData(false), Ec);

		if (Ec)
		{
			ZEN_WARN("Unable to grab lock at '{}' (reason: '{}'), retrying", LockFilePath, Ec.message());
			Sleep(500);

			m_LockFile.Create(LockFilePath, MakeLockData(false), Ec);
			if (Ec)
			{
				ZEN_WARN("ERROR: Unable to grab lock at '{}' (reason: '{}')", LockFilePath, Ec.message());
				std::exit(99);
			}
		}

		InitializeServerLogging(m_ServerOptions);

#if ZEN_USE_SENTRY
		Sentry.LogStartupInformation();
#endif

		MaximizeOpenFileCount();

		ZEN_INFO(ZEN_APP_NAME " - using lock file at '{}'", LockFilePath);

		ZEN_INFO(ZEN_APP_NAME " - starting on port {}, version '{}'", m_ServerOptions.BasePort, ZEN_CFG_VERSION_BUILD_STRING_FULL);

		Entry = ServerState.Register(m_ServerOptions.BasePort);

		if (m_ServerOptions.OwnerPid)
		{
			// We are adding a sponsor process to our own entry, can't wait for pick since the code is not run until later
			Entry->AddSponsorProcess(m_ServerOptions.OwnerPid, 0);
		}

		ZenServer Server;
		Server.SetDataRoot(m_ServerOptions.DataDir);
		Server.SetContentRoot(m_ServerOptions.ContentDir);
		Server.SetTestMode(m_ServerOptions.IsTest);
		Server.SetDedicatedMode(m_ServerOptions.IsDedicated);

		auto ServerCleanup = MakeGuard([&Server] { Server.Cleanup(); });

		int EffectiveBasePort = Server.Initialize(m_ServerOptions, Entry);

		Entry->EffectiveListenPort = uint16_t(EffectiveBasePort);
		if (EffectiveBasePort != m_ServerOptions.BasePort)
		{
			ZEN_INFO(ZEN_APP_NAME " - relocated to base port {}", EffectiveBasePort);
			m_ServerOptions.BasePort = EffectiveBasePort;
		}

		std::unique_ptr<std::thread> ShutdownThread;
		std::unique_ptr<NamedEvent>	 ShutdownEvent;

		ExtendableStringBuilder<64> ShutdownEventName;
		ShutdownEventName << "Zen_" << m_ServerOptions.BasePort << "_Shutdown";
		ShutdownEvent.reset(new NamedEvent{ShutdownEventName});

		// Monitor shutdown signals

		ShutdownThread.reset(new std::thread{[&] {
			SetCurrentThreadName("shutdown_monitor");

			ZEN_INFO("shutdown monitor thread waiting for shutdown signal '{}' for process {}",
					 ShutdownEventName,
					 zen::GetCurrentProcessId());

			if (ShutdownEvent->Wait())
			{
				ZEN_INFO("shutdown signal for pid {} received", zen::GetCurrentProcessId());
				Server.RequestExit(0);
			}
			else
			{
				ZEN_INFO("shutdown signal wait() failed");
			}
		}});

		auto CleanupShutdown = MakeGuard([&ShutdownEvent, &ShutdownThread] {
			if (ShutdownEvent)
			{
				ShutdownEvent->Set();
			}
			if (ShutdownThread && ShutdownThread->joinable())
			{
				ShutdownThread->join();
			}
		});

		// If we have a parent process, establish the mechanisms we need
		// to be able to communicate readiness with the parent

		Server.SetIsReadyFunc([&] {
			m_LockFile.Update(MakeLockData(true), Ec);

			if (!m_ServerOptions.ChildId.empty())
			{
				NamedEvent ParentEvent{m_ServerOptions.ChildId};
				ParentEvent.Set();
			}
		});

		Server.Run();
	}
	catch (const AssertException& AssertEx)
	{
		ZEN_CRITICAL("Caught assert exception in main for process {}: {}", zen::GetCurrentProcessId(), AssertEx.FullDescription());
		RequestApplicationExit(1);
	}
	catch (const std::exception& e)
	{
		ZEN_CRITICAL("Caught exception in main for process {}: {}", zen::GetCurrentProcessId(), e.what());
		RequestApplicationExit(1);
	}

	ShutdownServerLogging();

	return ApplicationExitCode();
}

//////////////////////////////////////////////////////////////////////////

#if ZEN_PLATFORM_WINDOWS

class ZenWindowsService : public WindowsService
{
public:
	ZenWindowsService(ZenServerOptions& ServerOptions) : m_EntryPoint(ServerOptions) {}

	ZenWindowsService(const ZenWindowsService&) = delete;
	ZenWindowsService& operator=(const ZenWindowsService&) = delete;

	virtual int Run() override;

private:
	ZenEntryPoint m_EntryPoint;
};

int
ZenWindowsService::Run()
{
	return m_EntryPoint.Run();
}

#endif	// ZEN_PLATFORM_WINDOWS

}  // namespace zen

//////////////////////////////////////////////////////////////////////////

#if ZEN_WITH_TESTS
int
test_main(int argc, char** argv)
{
	zen::zenserver_forcelinktests();

	zen::logging::InitializeLogging();
	zen::logging::SetLogLevel(zen::logging::level::Debug);

	zen::MaximizeOpenFileCount();

	return ZEN_RUN_TESTS(argc, argv);
}
#endif

int
main(int argc, char* argv[])
{
#if ZEN_USE_MIMALLOC
	mi_version();
#endif
	using namespace zen;

	if (argc >= 2)
	{
		if (argv[1] == "test"sv)
		{
#if ZEN_WITH_TESTS
			return test_main(argc, argv);
#else
			fprintf(stderr, "test option not available in release mode!\n");
			exit(5);
#endif
		}
	}

#if ZEN_PLATFORM_LINUX | ZEN_PLATFORM_MAC
	// Detach ourselves from any parent process
	setsid();
#endif

	signal(SIGINT, utils::SignalCallbackHandler);
	signal(SIGTERM, utils::SignalCallbackHandler);

	try
	{
		ZenServerOptions ServerOptions;
		ParseCliOptions(argc, argv, ServerOptions);

		std::string_view DeleteReason;

		if (ServerOptions.IsCleanStart)
		{
			DeleteReason = "clean start requested"sv;
		}
		else if (!ServerOptions.BaseSnapshotDir.empty())
		{
			DeleteReason = "will initialize state from base snapshot"sv;
		}

		if (!DeleteReason.empty())
		{
			if (std::filesystem::exists(ServerOptions.DataDir))
			{
				ZEN_CONSOLE_INFO("deleting files from '{}' ({})", ServerOptions.DataDir, DeleteReason);
				DeleteDirectories(ServerOptions.DataDir);
			}
		}

		if (!std::filesystem::exists(ServerOptions.DataDir))
		{
			ServerOptions.IsFirstRun = true;
			std::filesystem::create_directories(ServerOptions.DataDir);
		}

		if (!ServerOptions.BaseSnapshotDir.empty())
		{
			ZEN_CONSOLE_INFO("copying snapshot from '{}' into '{}", ServerOptions.BaseSnapshotDir, ServerOptions.DataDir);
			CopyTree(ServerOptions.BaseSnapshotDir, ServerOptions.DataDir, {.EnableClone = true});
		}

#if ZEN_WITH_TRACE
		if (ServerOptions.TraceHost.size())
		{
			TraceStart("zenserver", ServerOptions.TraceHost.c_str(), TraceType::Network);
		}
		else if (ServerOptions.TraceFile.size())
		{
			TraceStart("zenserver", ServerOptions.TraceFile.c_str(), TraceType::File);
		}
		else
		{
			TraceInit("zenserver");
		}
		atexit(TraceShutdown);
#endif	// ZEN_WITH_TRACE

#if ZEN_PLATFORM_WINDOWS
		if (ServerOptions.InstallService)
		{
			WindowsService::Install();

			std::exit(0);
		}

		if (ServerOptions.UninstallService)
		{
			WindowsService::Delete();

			std::exit(0);
		}

		ZenWindowsService App(ServerOptions);
		return App.ServiceMain();
#else
		if (ServerOptions.InstallService || ServerOptions.UninstallService)
		{
			throw std::runtime_error("Service mode is not supported on this platform");
		}

		ZenEntryPoint App(ServerOptions);
		return App.Run();
#endif
	}
	catch (const AssertException& AssertEx)
	{
		fprintf(stderr, "ERROR: Caught assert exception in main: '%s'", AssertEx.FullDescription().c_str());
		return 1;
	}
	catch (const std::exception& Ex)
	{
		fprintf(stderr, "ERROR: Caught exception in main: '%s'", Ex.what());

		return 1;
	}
}
